/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.ws.server;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;

import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.bmi.gwt.ws.server.serialization.SerializationTool;

import com.google.gwt.user.server.rpc.SerializationPolicy;
import com.google.gwt.user.server.rpc.SerializationPolicyLoader;
import com.google.gwt.user.server.rpc.SerializationPolicyProvider;

/**
 * Base class which should be extended by any web socket requiring to serialize
 * and deserialize objects.
 * 
 * @author Marco Tamburelli
 */
@SuppressWarnings("serial")
public abstract class AbstractWebSocketServer extends WebSocketServlet implements SerializationPolicyProvider
{
	/**
	 * A cache of moduleBaseURL and serialization policy strong name to
	 * {@link SerializationPolicy}.
	 */
	private final Map<String, SerializationPolicy> serializationPolicyCache = new HashMap<String, SerializationPolicy>();

	private transient ThreadLocal<String> perThreadContextPath;

	@Override
	public void init() throws ServletException
	{
		super.init();

		this.perThreadContextPath = new ThreadLocal<String>();
	}

	@Override
	public SerializationPolicy getSerializationPolicy(String moduleBaseURL, String serializationPolicyStrongName)
	{
		SerializationPolicy serializationPolicy = getCachedSerializationPolicy(moduleBaseURL,
				serializationPolicyStrongName);
		if (serializationPolicy != null) { return serializationPolicy; }

		serializationPolicy = loadSerializationPolicy(AbstractWebSocketServer.this, getContextPath(), moduleBaseURL,
				serializationPolicyStrongName);

		if (serializationPolicy == null)
		{
			/*
			 * Failed to get the requested serialization policy; use the default
			 */
			log("WARNING: Failed to get the SerializationPolicy '"
					+ serializationPolicyStrongName
					+ "' for module '"
					+ moduleBaseURL
					+ "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.");
			serializationPolicy = SerializationTool.getDefaultSerializationPolicy();
		}

		/*
		 * This could cache null or an actual instance. Either way we will not
		 * attempt to lookup the policy again.
		 */
		putCachedSerializationPolicy(moduleBaseURL, serializationPolicyStrongName, serializationPolicy);

		return serializationPolicy;
	}

	protected final String getContextPath()
	{
		synchronized (this)
		{
			return this.perThreadContextPath.get();
		}
	}

	protected final void setContextPath(String contextPath)
	{
		synchronized (this)
		{
			this.perThreadContextPath.set(contextPath);
		}
	}

	private SerializationPolicy getCachedSerializationPolicy(String moduleBaseURL, String strongName)
	{
		synchronized (this.serializationPolicyCache)
		{
			return this.serializationPolicyCache.get(moduleBaseURL + strongName);
		}
	}

	private void putCachedSerializationPolicy(String moduleBaseURL, String strongName,
			SerializationPolicy serializationPolicy)
	{
		synchronized (this.serializationPolicyCache)
		{
			this.serializationPolicyCache.put(moduleBaseURL + strongName, serializationPolicy);
		}
	}

	@Override
	protected final StreamInbound createWebSocketInbound(String subProtocol, HttpServletRequest request)
	{
		setContextPath(request.getContextPath());

		return doCreateWebSocketInbound(subProtocol, request);
	}

	/**
	 * This method should be overwritten. It processes the client request for a
	 * {@link ObjectInbound} object.
	 * 
	 * @param subProtocol
	 * @param request The HTTP request that initiated this WebSocket connection.
	 *        Note that this object is only valid inside this method. You must
	 *        not retain a reference to it outside the execution of this method.
	 *        If Tomcat detects such access, it will throw an
	 *        IllegalStateException
	 * @return
	 */
	public abstract ObjectInbound<?> doCreateWebSocketInbound(String subProtocol, HttpServletRequest request);

	static SerializationPolicy loadSerializationPolicy(HttpServlet servlet, String contextPath, String moduleBaseURL,
			String strongName)
	{
		String modulePath = null;
		if (moduleBaseURL != null)
		{
			try
			{
				modulePath = new URL(moduleBaseURL).getPath();
			}
			catch (MalformedURLException ex)
			{
				// log the information, we will default
				servlet.log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
			}
		}

		SerializationPolicy serializationPolicy = null;

		/*
		 * Check that the module path must be in the same web app as the servlet
		 * itself. If you need to implement a scheme different than this,
		 * override this method.
		 */
		if (modulePath == null || !modulePath.startsWith(contextPath))
		{
			String message = "ERROR: The module path requested, " + modulePath
					+ ", is not in the same web application as this servlet, " + contextPath
					+ ".  Your module may not be properly configured or your client and server code maybe out of date.";
			servlet.log(message);
		}
		else
		{
			/*
			 * Strip off the context path from the module base URL. It should be
			 * a strict prefix.
			 */
			String contextRelativePath = modulePath.substring(contextPath.length());

			String serializationPolicyFilePath = SerializationPolicyLoader
					.getSerializationPolicyFileName(contextRelativePath + strongName);

			/* Open the RPC resource file and read its contents. */
			InputStream is = servlet.getServletContext().getResourceAsStream(serializationPolicyFilePath);
			try
			{
				if (is != null)
				{
					try
					{
						serializationPolicy = SerializationPolicyLoader.loadFromStream(is, null);
					}
					catch (ParseException e)
					{
						servlet.log("ERROR: Failed to parse the policy file '" + serializationPolicyFilePath + "'", e);
					}
					catch (IOException e)
					{
						servlet.log("ERROR: Could not read the policy file '" + serializationPolicyFilePath + "'", e);
					}
				}
				else
				{
					String message = "ERROR: The serialization policy file '" + serializationPolicyFilePath
							+ "' was not found; did you forget to include it in this deployment?";
					servlet.log(message);
				}
			}
			finally
			{
				if (is != null)
				{
					try
					{
						is.close();
					}
					catch (IOException e)
					{
						// Ignore this error
					}
				}
			}
		}

		return serializationPolicy;
	}
}
